23장. 예외 처리
프로그램은 늘 예상대로만 흐르지 않습니다.
파일이 없을 수도 있고,
숫자가 들어와야 할 자리에 글자가 올 수도 있고,
네트워크가 끊길 수도 있습니다.
이렇게 “정상적이지 않은 상황“을
예외(Exception)라고 부릅니다.
이 장에서는 예외를 다루는 기본 문법과,
백엔드에서 오류를 어떻게 설계하면 좋은지 살펴봅니다.
23.1 try, catch, finally
예외가 발생할 수 있는 코드는try 블록으로 감쌉니다.
예외가 발생하면 catch 블록이 대신 실행됩니다.
fun parseNumber(text: String) {
try {
val number = text.toInt()
println("숫자: $number")
} catch (e: NumberFormatException) {
println("숫자로 바꿀 수 없습니다: $text")
}
}
parseNumber("123") // 숫자: 123
parseNumber("abc") // 숫자로 바꿀 수 없습니다: abc
구조를 정리하면 이렇습니다.
try: 예외가 생길 수 있는 코드catch: 예외가 생겼을 때 실행할 코드finally: 예외와 상관없이 항상 실행할 코드
finally는 뒷정리에 씁니다.
try {
println("작업 시작")
throw RuntimeException("문제 발생")
} catch (e: Exception) {
println("예외 처리: ${e.message}")
} finally {
println("항상 실행되는 정리 코드")
}
finally는 예외가 나든 안 나든
반드시 실행됩니다.
23.2 Expression으로서의 try
3장에서 if와 when이
값을 돌려주는 표현식이라고 배웠습니다.
코틀린에서는 try도 표현식입니다.
즉, 결과를 값으로 받을 수 있습니다.
val number = try {
text.toInt()
} catch (e: NumberFormatException) {
0 // 실패하면 기본값 0
}
try가 성공하면 그 결과가,
실패하면 catch의 결과가 number에 담깁니다.
이 방식은 코드를 훨씬 간결하게 만들어 줍니다.
23.3 throw
예외를 직접 발생시키려면 throw를 씁니다.
fun checkAge(age: Int) {
if (age < 0) {
throw IllegalArgumentException("나이는 음수일 수 없습니다: $age")
}
println("나이: $age")
}
여기서 흥미로운 점이 있습니다.
코틀린에서는 throw도 표현식입니다.
그래서 엘비스 연산자(?:)와 함께
자주 쓰입니다.
val name = user.name ?: throw IllegalStateException("이름이 없습니다")
user.name이 null이면
바로 예외를 던지는 코드입니다.
(엘비스 연산자는 5장에서 배웠습니다.)
23.4 Kotlin에는 Checked Exception이 없다
자바를 아는 분이라면
이 부분이 반가울 것입니다.
자바에는 검사 예외(Checked Exception)가 있습니다.
특정 예외는 반드시 try-catch로 처리하거나throws로 선언해야 했습니다.
// 자바
public void readFile() throws IOException {
// ...
}
이 규칙은 안전을 위한 것이었지만,
실제로는 의미 없는 try-catch를 양산하기도 했습니다.
코틀린에는 검사 예외가 없습니다.
모든 예외를 강제로 처리하지 않아도 됩니다.
fun readFile() {
// throws 선언이 필요 없음
}
덕분에 코드가 깔끔해집니다.
다만 그만큼 “어떤 예외를 처리할지“는
개발자가 스스로 판단해야 합니다.
23.5 Custom Exception
기본 예외만으로 부족할 때가 있습니다.
우리 서비스에 맞는 예외를 직접 만들 수 있습니다.
Exception을 상속하면 됩니다.
class UserNotFoundException(
val userId: Long
) : RuntimeException("회원을 찾을 수 없습니다: $userId")
이렇게 만든 예외는 이렇게 사용합니다.
fun findUser(id: Long): User {
return userRepository.find(id)
?: throw UserNotFoundException(id)
}
직접 만든 예외의 장점은
“무엇이 잘못됐는지“가 이름만으로 드러난다는 점입니다.
UserNotFoundException은RuntimeException보다 훨씬 명확합니다.
23.6 예외 메시지와 오류 정보 설계
예외를 던질 때는
“나중에 이 메시지를 볼 사람“을 생각해야 합니다.
좋은 예외 메시지는 두 가지를 담습니다.
- 무엇이 잘못됐는가
- 어떤 값 때문에 잘못됐는가
나쁜 예와 좋은 예를 비교해 봅시다.
// 나쁜 예: 원인을 알 수 없음
throw IllegalArgumentException("잘못된 값")
// 좋은 예: 원인과 값이 드러남
throw IllegalArgumentException("주문 수량은 1 이상이어야 합니다. 입력값: $quantity")
또한 예외에 필요한 정보를
프로퍼티로 담아 두면
나중에 코드로 활용하기 좋습니다.
class PaymentException(
val orderId: Long,
val reason: String
) : RuntimeException("결제 실패 [주문 $orderId]: $reason")
예외는 단순히 “터뜨리는 것“이 아니라,
“무슨 일이 있었는지 남기는 기록“입니다.
이렇게 설계해 두면
오류를 추적하고 대응하기가 훨씬 쉬워집니다.
23장을 마치며
이 장에서 우리는 다음을 배웠습니다.
try,catch,finally의 기본 구조try와throw가 값을 돌려주는 표현식이라는 점- 코틀린에는 검사 예외가 없어 코드가 간결하다는 점
- 서비스에 맞는 예외를 직접 만드는 방법
- 원인과 값을 담은 좋은 예외 메시지 설계
예외는 “실패를 표현하는 한 가지 방법“입니다.
다음 장에서는 예외 말고도
실패를 표현하는 여러 방법을 비교해 봅니다.